feat(attestation)!: pin verification to archived collateral with VerifyMode - #93
feat(attestation)!: pin verification to archived collateral with VerifyMode#93samlaf wants to merge 2 commits into
Conversation
ameba23
left a comment
There was a problem hiding this comment.
⭐ Generally looks good but i would prefer to have passing in collateral as being a special method for a special purpose.
| // every call site for a value that is never stored. | ||
| #[allow(clippy::large_enum_variant)] | ||
| #[derive(Clone, Debug, PartialEq, Eq)] | ||
| pub enum VerifyMode { |
There was a problem hiding this comment.
After discussing this with others at our meeting, i think the archived mode should be a separate method rather than passing an enum to verify_attestation.
That is verifiy_attestation stays as is, and add a separate method call something like verify_attestation_ator verify_attestation_with_collateral.
There was a problem hiding this comment.
Done in the latest commit. Prob easier to just compare diff against main however, since it's now pretty drastically different from the previous version with the enum. LMKWYT.
There was a problem hiding this comment.
With 'archive mode' (given collateral) we don't need to bail here if no pccs.
There was a problem hiding this comment.
This is no longer an issue since #88 made pccs mandatory
…fyMode Closes flashbots#84 Second of two changes for that issue, on top of flashbots#85. Reporting the endorsements a verification consumed is half of provenance. The other half is running the same verification again later, against that snapshot, and getting the same answer. Nothing exposed that: the public entry points always fetched and always read the wall clock, and the only way to supply a bundle was through variants that also took a bare timestamp. API before and after -------------------- AttestationVerifier verify_attestation(msg, input) -> verify_attestation(msg, input, mode) verify_attestation_sync(msg, input) -> verify_attestation_sync(msg, input, mode) dcap verify_dcap_attestation(q, input, pccs) -> verify_dcap_attestation(q, input, mode, pccs) verify_dcap_attestation_sync(q, input, pccs) -> verify_dcap_attestation_sync(q, input, mode, pccs) verify_dcap_attestation_with_given_timestamp( q, input, pccs, Option<collateral>, now, override_azure_outdated_tcb) -> (removed) verify_dcap_attestation_with_timestamp_sync( q, input, pccs, Option<collateral>, now, override_azure_outdated_tcb) -> (removed) azure verify_azure_attestation(a, input, pccs, override) -> verify_azure_attestation(a, input, mode, pccs, override) verify_azure_attestation_sync(a, input, pccs, override) -> verify_azure_attestation_sync(a, input, mode, pccs, override) new enum VerifyMode { Live, Archived(EndorsementSnapshot) } DcapVerificationError::ArchivedWithoutDcapCollateral Why this shape -------------- One input instead of two. The removed variants took the collateral and the instant as separate arguments, so a caller could pair a pinned bundle with the wrong instant, or a live fetch with a pinned instant, and get a verdict that reproduces nothing. VerifyMode::Archived takes the EndorsementSnapshot that flashbots#85 hands back, so the bundle and the instant it was held to travel together and the mistake has no spelling. The mixed case is refused too: an archived snapshot with no bundle for the DCAP leg fails with ArchivedWithoutDcapCollateral rather than being completed by a fetch. The mode reaches the verifier. The measurement-policy check lives on AttestationVerifier, and a relying party re-checking archived evidence needs both it and the pinned instant. The removed variants sat below the verifier, so that combination did not exist. The mode is a parameter of the call rather than the builder because it is a fact about one verification, not about the verifier: the same instance serves a live handshake and an archive replay. One instant for both Azure legs. The DCAP leg reports the instant it evaluated at, and the vTPM AK chain is checked at that same instant, in either mode. The wall clock is read in exactly one place. The Azure TCB override leaves the public surface. It rode along on the removed variants only because the Azure verifier and the fixture tests shared them. Both now call the crate-private body that the two public entry points wrap, so the override is an argument of the Azure leg and nothing else. Only Azure has a reason to relax TCB checks. Live is behaviour-preserving. Every existing caller passes VerifyMode::Live and gets what it got before: collateral from the PCCS or Intel, freshness at the wall clock. The two in-tree callers, attested-tls and attestation-provider-server, needed only that argument. GCP checks and Archived mode ---------------------------- The GCP host provenance check from flashbots#54 stays live in either mode, as does the firmware fetch for the quote's MRTD. Neither rests on signed material a replay could re-verify: the provenance document is an unsigned JSON object whose trust is the TLS connection to Google's bucket, so archiving it would not make a replay stronger. The docs on VerifyMode::Archived and verify_attestation state the carve-out. Whether Archived should skip the provenance lookup instead is left open. BREAKING CHANGE: verify_attestation and verify_attestation_sync take a VerifyMode; the DCAP and Azure entry points take mode before pccs; the *_with_given_timestamp variants are gone, replaced by VerifyMode::Archived, which fails with DcapVerificationError::ArchivedWithoutDcapCollateral when its snapshot carries no DCAP bundle.
d970661 to
521fa50
Compare
…VerifyMode Review on flashbots#93 asked for the archived case to be its own method rather than an enum threaded through verify_attestation. This does that, and drops the mode everywhere it had reached. API --- AttestationVerifier verify_attestation(msg, input, mode) -> verify_attestation(msg, input) as on main verify_attestation_sync(msg, input, mode) -> verify_attestation_sync(msg, input) as on main verify_attestation_archived(msg, input, &EndorsementSnapshot) new dcap verify_dcap_attestation(q, input, mode, pccs) -> verify_dcap_attestation(q, input, pccs) as on main verify_dcap_attestation_sync(q, input, mode, pccs) -> verify_dcap_attestation_sync(q, input, pccs) as on main verify_dcap_attestation_archived(q, input, &snapshot) new azure verify_azure_attestation(a, input, mode, pccs, override) -> (…, pccs, override) as on main verify_azure_attestation_sync(a, input, mode, pccs, override) -> (…, pccs, override) as on main verify_azure_attestation_archived(a, input, &snapshot, override) new enum VerifyMode removed DcapVerificationError::ArchivedWithoutDcapCollateral kept Why a method ------------ Live verification and archive replay are different operations a relying party does in different places, never both per call, so a parameter that selects between them buys nothing and costs every live caller a VerifyMode::Live. Keeping them apart leaves the live signatures exactly as main has them and lets the replay state its own contract. The snapshot stays the input. The archived methods take the EndorsementSnapshot that flashbots#85 hands back, not bare collateral, so the bundle and the instant it was held to travel together. A snapshot with no DCAP bundle is still refused with ArchivedWithoutDcapCollateral rather than completed by a fetch. This is deliberately not the case where collateral arrives out of band and is checked at the wall clock, as flashbots#65 proposes; that would be a live method. Synchronous. A replay fetches nothing time-dependent, so it needs no runtime and has no async twin. The Azure AK chain is checked at the snapshot's instant too, so one instant sits behind every freshness check. GCP --- The previous version kept the provenance lookup live in archived mode and left "should it?" open. It is now skipped on replay. The registry is unsigned and mutable, so a replay could only learn what it says today, and if an entry disappeared a sound archive would stop verifying, which is the failure the archive exists to prevent. The original verification already consulted it. Firmware for a portable-image policy is still fetched on a cache miss: it is signed by Google and content-addressed by the quote's MRTD, so the fetch cannot change the verdict. Recording the provenance outcome and the firmware in the snapshot would remove both caveats and is left for a later change. Internals --------- resolve_mode and the fallback-collateral plumbing go. The three crate-private bodies verify_quote, verify_quote_sync and verify_quote_archived each know their own source and reach one collateral-in-hand verifier, still parameterised on the dcap-qvl QuoteVerifier so mock builds replay against the mock root. The mock live entry points with a passthrough PCCS reuse the archived body with the embedded bundle at the wall clock, which is what they always did in effect. The Azure verifier splits its prepared evidence into the TD quote and the vTPM leg, so its three entry points share everything but the DCAP call, and the finish function now assembles the VerifiedAttestation and reads the instant from the DCAP leg in one place. The two verifier methods that need no runtime share one body that differs only in where the DCAP leg gets its endorsements and whether provenance runs. Module docs on dcap and azure::verify say which situation each entry point is for before saying how they differ. The README gains a paragraph on the snapshot and the archived method. Tests: the fixture replays move from the removed variants onto the archived path; VerifyMode::Live disappears from every live call; two new verifier-level tests replay a live mock verdict and check that a GCP replay does not consult the registry. attested-tls and attestation-provider-server are back to their main versions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Rebased on top of the new main and fixed the issues. Then created a new commit with the requested API change. |
Fixes the second half of #84. First half was #85.
TLDR: we archive the evidence from our genesis machines together with the
EndorsementSnapshotthat #85 made verification report, and later we need tore-verify that evidence and get the same answer. Today nothing exposes that: the
public entry points always fetch collateral and always check freshness at the
wall clock, so about a month after collection a perfectly good archive stops
verifying. This adds an archived verification method that takes the snapshot
back.
Addressing review
The first commit did this as a
VerifyModeenum threaded throughverify_attestation. Per the review, it is now a separate method and the livesignatures are exactly as on main. The second commit is that rewrite; the diff
against main is what to review.
The other review comment, about bailing on a missing PCCS in the sync path, was
resolved by #88 making the PCCS mandatory.
API
AttestationVerifier
verify_attestation(msg, input) unchanged
verify_attestation_sync(msg, input) unchanged
verify_attestation_archived(msg, input, &EndorsementSnapshot) new
dcap
verify_dcap_attestation(q, input, pccs) unchanged
verify_dcap_attestation_sync(q, input, pccs) unchanged
verify_dcap_attestation_archived(q, input, &snapshot) new
verify_dcap_attestation_with_given_timestamp(...) removed
verify_dcap_attestation_with_timestamp_sync(...) removed
azure
verify_azure_attestation(a, input, pccs, override) unchanged
verify_azure_attestation_sync(a, input, pccs, override) unchanged
verify_azure_attestation_archived(a, input, &snapshot, override) new
DcapVerificationError::ArchivedWithoutDcapCollateral new
What archived means
The three entry points on each platform are one per situation a relying party
is in. The async one is for a live handshake with a runtime. The sync one is
for a live handshake inside a callback that cannot await, such as a rustls
verifier, and can only read the PCCS cache. The archived one is for re-checking
evidence long after the fact, against the snapshot its original verification
reported: it fetches nothing and evaluates every freshness check at the
snapshot's instant rather than now. On Azure that includes the AK certificate
chain, so one instant sits behind both legs.
It takes the
EndorsementSnapshot, not bare collateral. The removed_with_given_timestampvariants took collateral and an instant as twoarguments, so a caller could pair a pinned bundle with the wrong instant, or a
live fetch with a pinned instant, and get a verdict that reproduces nothing.
With the snapshot the bundle and the instant it was held to travel together. A
snapshot with no DCAP bundle is refused with
ArchivedWithoutDcapCollateralrather than completed by a fetch.
This is deliberately not the case where collateral arrives out of band and is
checked at the wall clock, as #65 proposes. That is a live verification and
would be its own method; the snapshot's instant is what makes this one a
replay.
GCP
This is a behaviour change from the version reviewed, which kept both GCP
checks live in archived mode.
The provenance lookup against Google's PPID registry is skipped on replay.
The registry is unsigned and mutable, so a replay could only learn what it says
today, and if an entry disappeared a sound archive would stop verifying, which
is the failure the archive exists to prevent. The original verification already
consulted it.
Firmware for a portable-image policy is still fetched on a cache miss. It is
signed by Google and content-addressed by the quote's MRTD, so the fetch cannot
change the verdict. Skipping it would make every GCP archive with an image-hash
policy fail in a fresh process, which is a silent trap.
So the contract is "fetches nothing whose answer can change over time" rather
than "fetches nothing". Recording the provenance outcome and the firmware in
the snapshot would remove both caveats and is left for a later change, as is
reporting the compiled-in trust anchors from the #84 discussion.
Internals
The five timestamp-flavoured DCAP and Azure functions collapse into three
crate-private bodies,
verify_quote,verify_quote_syncandverify_quote_archived, that each know where their collateral and instant comefrom and reach one collateral-in-hand verifier. That verifier is parameterised
on the
dcap-qvlQuoteVerifier, so mock builds replay against the mock rootthe same way they verify live.
The Azure verifier splits its prepared evidence into the TD quote and the vTPM
leg, so its three entry points share everything but the DCAP call, and the
finish function assembles the
VerifiedAttestationand reads the instant fromthe DCAP leg in one place.
On
AttestationVerifier, the sync and archived methods share one body thatdiffers only in where the DCAP leg gets its endorsements and whether the
provenance lookup runs. The async path is untouched.
Tests
The fixture tests that replayed real captures through the removed variants now
go through the archived path. Two new verifier-level tests replay a live mock
verdict through
verify_attestation_archivedand check that measurements,matched policy and snapshot all agree, and check that a GCP-typed replay does
not consult the registry (a mock PPID is not in it, so the lookup would fail
closed if it ran).
BREAKING CHANGE:
verify_dcap_attestation_with_given_timestampandverify_dcap_attestation_with_timestamp_syncare removed, replaced byverify_dcap_attestation_archived. Every other public signature is as on main.